package data

import (
	"context"
	"strings"
	"time"

	"kratos-layout/app/helloworld/internal/biz"
	"kratos-layout/app/helloworld/internal/conf"
	"kratos-layout/pkg/gormc"

	"github.com/go-kratos/kratos/v2/log"
	"github.com/google/wire"
	"github.com/redis/go-redis/v9"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
)

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
	// generated start
	NewData,
	NewGreeterRepo,
	// generated end
)

// transation
type contextTxKey struct{}

// Data .
type Data struct {
	db    *gorm.DB
	rdb   redis.UniversalClient
	cache *gormc.GormCache
	log   *log.Helper
}

// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
	log := log.NewHelper(logger)
	db, err := getDB(c.Database, logger)
	if err != nil {
		return nil, nil, err
	}
	rdb, err := getRedis(c.Redis, logger)
	if err != nil {
		return nil, nil, err
	}
	cleanup := func() {
		log.Info("closing the data resources")
		// if err := db.Close(); err != nil {
		// 	log.Error(err)
		// }
		if err := rdb.Close(); err != nil {
			log.Error(err)
		}
	}
	// migrate
	autoMigrate(db)
	//
	return &Data{
		db:    db,
		rdb:   rdb,
		cache: gormc.NewGormCache(db, rdb),
		log:   log,
	}, cleanup, nil
}

// NewTransaction .
func NewTransaction(d *Data) biz.Transaction {
	return d
}

func (d *Data) InTx(ctx context.Context, fn func(ctx context.Context) error) error {
	return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		ctx = context.WithValue(ctx, contextTxKey{}, tx)
		return fn(ctx)
	})
}

func (d *Data) DB(ctx context.Context) *gorm.DB {
	tx, ok := ctx.Value(contextTxKey{}).(*gorm.DB)
	if ok {
		return tx
	}
	return d.db
}

func getDB(c *conf.Data_Database, log log.Logger) (*gorm.DB, error) {
	db, err := gorm.Open(mysql.Open(c.Source), &gorm.Config{
		Logger: getDBLogger(log),
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
			TablePrefix:   c.TablePrefix,
		},
		DisableForeignKeyConstraintWhenMigrating: true,
	})
	if err != nil {
		return nil, err
	}
	sqlDB, err := db.DB()
	if err != nil {
		return nil, err
	}
	sqlDB.SetMaxOpenConns(int(c.MaxOpenConns))
	sqlDB.SetMaxIdleConns(int(c.MaxIdleConns))
	return db, nil
}

func getRedis(c *conf.Data_Redis, log log.Logger) (redis.UniversalClient, error) {
	addrs := strings.Split(c.Addr, ",")
	client := redis.NewUniversalClient(&redis.UniversalOptions{
		Addrs:        addrs,
		Password:     c.Password,
		ReadTimeout:  c.ReadTimeout.AsDuration(),
		WriteTimeout: c.WriteTimeout.AsDuration(),
		MasterName:   c.MasterName,
		DB:           int(c.Db),
	})
	_, err := client.Ping(context.Background()).Result()
	if err != nil {
		return nil, err
	}
	return client, nil
}

func getDBLogger(log log.Logger) logger.Interface {
	slowLogger := gormc.NewLogger(
		// TODO
		// gormc.WithZapLogger(log),
		gormc.WithLogLevel(logger.Info),
		gormc.WithSlowThreshold(1*time.Millisecond),
		gormc.WithRequestId(true),
	)
	return slowLogger
}
